/*
 * Copyright 2008 Alberto Gimeno <gimenete at gmail.com>
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package siena.jdbc;

import java.lang.reflect.Field;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import siena.ClassInfo;
import siena.DateTime;
import siena.IterableQuery;
import siena.Query;
import siena.SienaException;
import siena.SimpleDate;
import siena.Time;
import siena.Util;
import siena.jdbc.JdbcPersistenceManager.JdbcClassInfo;

public class JdbcQuery<T> implements Query<T> {

	private Class<T> clazz;
	private JdbcPersistenceManager pm;
	private List<String> filter;
	private List<String> order;
	private List<Object> parameters;

	private static final String[] supportedOperators = { "<", ">", ">=", "<=", "=", "IN" };
	
	private int nextOffset;
	
	private JdbcQuery() {
	}

	public JdbcQuery(Class<T> clazz, JdbcPersistenceManager pm) {
		this.clazz = clazz;
		this.pm = pm;
	}

	public List<T> fetch() {
		List<T> result = pm.fetch(clazz, filter, parameters, order);
		nextOffset = result.size();
		return result;
	}

	public List<T> fetch(int limit) {
		List<T> result = pm.fetch(clazz, filter, parameters, order, limit);
		nextOffset = result.size();
		return result;
	}

	public List<T> fetch(int limit, Object offset) {
		nextOffset = (Integer) offset;
		List<T> result = pm.fetch(clazz, filter, parameters, order, limit, offset);
		nextOffset += result.size();
		return result;
	}
	
	public Iterable<T> iter(String field, int max) {
		return new IterableQuery<T>(this, max, field);
	}
	
	public Query<T> search(String match, boolean inBooleanMode,
			String... fieldNames) {
		if(filter == null) {
			filter = new ArrayList<String>();
		}
		if(parameters == null)
			parameters = new ArrayList<Object>();
		try {
			List<String> cols = new ArrayList<String>();
			for (String field : fieldNames) {
				Field f = clazz.getDeclaredField(field);
				String[] columns = ClassInfo.getColumnNames(f);
				for (String string : columns) {
					cols.add(string);
				}
			}
			if(inBooleanMode)
				filter.add("MATCH("+Util.join(cols, ",")+") AGAINST(? IN BOOLEAN MODE)");
			else
				filter.add("MATCH("+Util.join(cols, ",")+") AGAINST(?)");
			parameters.add(match);
		} catch (Exception e) {
			throw new SienaException(e);
		}
		return this;
	}

	public JdbcQuery<T> filter(String field, Object value) {
		if(filter == null) {
			filter = new ArrayList<String>();
		}
		if(parameters == null)
			parameters = new ArrayList<Object>();
		String op = "=";
		for (String operator : supportedOperators) {
			if(field.endsWith(operator)) {
				op = operator;
				field = field.substring(0, field.length()-operator.length());
				break;
			}
		}
		field = field.trim();
		try {
			Field f = clazz.getDeclaredField(field);
			String[] columns = ClassInfo.getColumnNames(f);
			if(op.equals("IN")) {
				if(!(value instanceof Collection<?>))
					throw new SienaException("Collection needed when using IN operator in filter() query");
				StringBuilder s = new StringBuilder();
				Collection<?> col = (Collection<?>) value;
				for (Object object : col) {
					// TODO: if object isModel
					parameters.add(object);
					s.append(",?");
				}
				filter.add(columns[0]+" IN("+s.toString().substring(1)+")");
			} else if(ClassInfo.isModel(f.getType())) {
				if(!op.equals("=")) {
					throw new SienaException("Unsupported operator for relationship: "+op);
				}
				JdbcClassInfo classInfo = pm.getClassInfo(f.getType());
				int i = 0;
				pm.checkForeignKeyMapping(classInfo.keys, columns, clazz, f);
				for (Field key : classInfo.keys) {
					if(value == null) {
						filter.add(columns[i++]+" IS NULL");
					} else {
						filter.add(columns[i++]+"=?");
						key.setAccessible(true);
						Object o = key.get(value);
						parameters.add(o);
					}
				}
			} else {
				if(value == null && op.equals("=")) {
					filter.add(columns[0]+" IS NULL");
				} else {
					filter.add(columns[0]+op+"?");
					if(value == null) {
						parameters.add(Types.NULL);
					} else {
						if (value instanceof Date) {
							value = translateDate(f, (Date) value);
						}
						parameters.add(value);
					}
				}
			}
		} catch (Exception e) {
			throw new SienaException(e);
		}
		return this;
	}

	private Object translateDate(Field f, Date value) {
		long t = value.getTime();
		
		SimpleDate simpleDate = f.getAnnotation(SimpleDate.class);
		if(simpleDate != null) {
			return new java.sql.Date(t);
		}
		
		DateTime dateTime = f.getAnnotation(DateTime.class);
		if(dateTime != null) {
			return new java.sql.Timestamp(t); 
		}
		
		Time time = f.getAnnotation(Time.class);
		if(time != null) {
			return new java.sql.Time(t); 
		}
		
		return new java.sql.Timestamp(t);
	}

	public T get() {
		List<T> list = fetch(1);
		if(list.isEmpty())
			return null;
		return list.get(0);
	}

	public JdbcQuery<T> order(String field) {
		if(order == null) {
			order = new ArrayList<String>();
		}
		if(field.startsWith("-")) {
			field = field.substring(1);
			String[] columns = pm.getColumnNames(clazz, field);
			for (String column : columns) {
				order.add(column+" DESC");
			}
		} else {
			String[] columns = pm.getColumnNames(clazz, field);
			for (String column : columns) {
				order.add(column);
			}
		}
		return this;
	}

	public int count() {
		return pm.count(clazz, filter, parameters);
	}

	public int count(int limit) {
		return pm.count(clazz, filter, parameters, limit);
	}

	public int count(int limit, Object offset) {
		return pm.count(clazz, filter, parameters, limit, offset);
	}
	
	public JdbcQuery<T> clone() {
		JdbcQuery<T> t = new JdbcQuery<T>();
		t.clazz = this.clazz;
		t.pm = this.pm;
		if(this.filter != null)
			t.filter = new ArrayList<String>(this.filter);
		if(this.order != null)
			t.order = new ArrayList<String>(this.order);
		if(this.parameters != null)
			t.parameters = new ArrayList<Object>(this.parameters);
		return t;
	}
	
	@Override
	public Object nextOffset() {
		return nextOffset;
	}
	
	@Override
	public int delete() {
		return pm.delete(clazz, filter, parameters);
	}
	
	@Override
	public List<T> fetchKeys() {
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public List<T> fetchKeys(int limit) {
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public List<T> fetchKeys(int limit, Object offset) {
		// TODO Auto-generated method stub
		return null;
	}
	
}
